# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.

# This source code is licensed under the license found in the
# LICENSE file in the root directory of this source tree.

import gc
from tkinter import W
from typing import Any, Dict, List, Optional, Tuple

import hydra
import pytorch_lightning as L
import rootutils
import torch
import signal  # noqa: F401
from pytorch_lightning import Callback, LightningDataModule, LightningModule, Trainer
from pytorch_lightning.callbacks import ModelCheckpoint
from pytorch_lightning.loggers import Logger
from omegaconf import DictConfig, OmegaConf

rootutils.setup_root(__file__, indicator=".project-root", pythonpath=True)
# ------------------------------------------------------------------------------------ #
# the setup_root above is equivalent to:
# - adding project root dir to PYTHONPATH
#       (so you don't need to force user to install project as a package)
#       (necessary before importing any local modules e.g. `from src import utils`)
# - setting up PROJECT_ROOT environment variable
#       (which is used as a base for paths in "configs/paths/default.yaml")
#       (this way all filepaths are the same no matter where you run the code)
# - loading environment variables from ".env" in root dir
#
# you can remove it if you:
# 1. either install project as a package or move entry files to project root dir
# 2. set `root_dir` to "." in "configs/paths/default.yaml"
#
# more info: https://github.com/ashleve/rootutils
# ------------------------------------------------------------------------------------ #

from fast3r.utils import (
    RankedLogger,
    extras,
    get_metric_value,
    instantiate_callbacks,
    instantiate_loggers,
    log_hyperparameters,
    task_wrapper,    
)
from fast3r.models import *
from fast3r.dust3r.loss import *
from fast3r.utils.utils import wait_for_gpu_memory, wait_for_python_gpu_processes

log = RankedLogger(__name__, rank_zero_only=True)

def python_eval_resolver(code: str):
    return eval(code)

import os
# Set the environment variable before Hydra initializes
os.environ['HYDRA_FULL_ERROR'] = '1'
# Register the resolver with OmegaConf
# usage: ${python_code:1 + 1} in yaml
OmegaConf.register_new_resolver("python_eval", python_eval_resolver)


@task_wrapper
def train(cfg: DictConfig) -> Tuple[Dict[str, Any], Dict[str, Any]]:
    """Trains the model. Can additionally evaluate on a testset, using best weights obtained during
    training.

    This method is wrapped in optional @task_wrapper decorator, that controls the behavior during
    failure. Useful for multiruns, saving info about the crash, etc.

    :param cfg: A DictConfig configuration composed by Hydra.
    :return: A tuple with metrics and dict with all instantiated objects.
    """
    # set seed for random number generators in pytorch, numpy and python.random
    if cfg.get("seed"):
        L.seed_everything(cfg.seed, workers=True)

    log.info(f"Instantiating datamodule <{cfg.data.data_module._target_}>")
    datamodule: LightningDataModule = hydra.utils.instantiate(cfg.data.data_module)

    log.info(f"Instantiating model <{cfg.model._target_}>")
    model: LightningModule = hydra.utils.instantiate(cfg.model)
    # print(model)

    log.info("Instantiating callbacks...")
    callbacks: List[Callback] = instantiate_callbacks(cfg.get("callbacks"))

    log.info("Instantiating loggers...")
    logger: List[Logger] = instantiate_loggers(cfg.get("logger"))

    log.info(f"Instantiating trainer <{cfg.trainer._target_}>")
    trainer: Trainer = hydra.utils.instantiate(cfg.trainer, callbacks=callbacks, logger=logger)

    object_dict = {
        "cfg": cfg,
        "datamodule": datamodule,
        "model": model,
        "callbacks": callbacks,
        "logger": logger,
        "trainer": trainer,
    }

    if logger:
        log.info("Logging hyperparameters!")
        log_hyperparameters(object_dict)

    # if cfg.get("test"):
    #     log.info("Starting testing!")
    #     ckpt_path = trainer.checkpoint_callback.best_model_path
    #     if ckpt_path == "":
    #         log.warning("Best ckpt not found! Using current weights for testing...")
    #         ckpt_path = None
    #     trainer.validate(model=model, datamodule=datamodule, ckpt_path=ckpt_path)
    #     log.info(f"Best ckpt path: {ckpt_path}")

    if cfg.get("train"):
        log.info("Starting training!")
        trainer.fit(model=model, datamodule=datamodule, ckpt_path=cfg.get("ckpt_path"))

    train_metrics = trainer.callback_metrics.copy()
    log.info("Training finished!")
    model.eval()
    model.cpu()
    # 3. Clear model gradients
    model.zero_grad(set_to_none=True)

    # 4. CRITICAL: Break the reference from the model to the trainer to free optimizer states
    if hasattr(model, "trainer") and model.trainer is not None:
        log.info("Surgically clearing optimizer states to release memory...")
        for optimizer in model.trainer.optimizers:
            optimizer.state.clear()
        model.trainer = None

    if hasattr(datamodule, "teardown"):
        log.info("Tearing down the 'fit' stage of the datamodule...")
        datamodule.teardown("fit")
        datamodule.teardown("validate")
        

    model.pretrained = None
    if cfg.get("test"):
        log.info("Starting test!")
        checkpoint_callback = next(cb for cb in trainer.callbacks if isinstance(cb, ModelCheckpoint))
        if checkpoint_callback:
            ckpt_path = checkpoint_callback.best_model_path
        else:
            ckpt_path = ""
        if ckpt_path == "":
            log.warning("Best ckpt not found! Using current weights for testing...")
            ckpt_path = None
        model.resume_from_checkpoint = True
                
        del trainer, callbacks
        if torch.cuda.is_available():
            torch.cuda.empty_cache()
        gc.collect()

        # logger: List[Logger] = instantiate_loggers(cfg.get("logger")['csv'])        
        test_trainer: Trainer = hydra.utils.instantiate(cfg.trainer, callbacks=None, logger=logger)
        
        test_trainer.test(model=model, datamodule=datamodule, ckpt_path=ckpt_path)
        log.info(f"Best ckpt path: {ckpt_path}")

        datamodule.teardown("test")
        test_metrics = test_trainer.callback_metrics
        metric_dict = {**train_metrics, **test_metrics}
    else:
        metric_dict = train_metrics
    
    model.cpu()
    del test_trainer, datamodule, model    
    return metric_dict, None


@hydra.main(version_base="1.3", config_path="../configs", config_name="train.yaml")
def main(cfg: DictConfig) -> Optional[float]:
    """Main entry point for training.

    :param cfg: DictConfig configuration composed by Hydra.
    :return: Optional[float] with optimized metric value.
    """
    # wait_for_gpu_memory(target_usage_gb=1, poll_interval_minutes=6)
    wait_for_python_gpu_processes(poll_interval_minutes=5)
    
    extras(cfg)
    metric_dict, _ = train(cfg)
    metric_value = get_metric_value(
        metric_dict=metric_dict, metric_name=cfg.get("optimized_metric")
    )
    del metric_dict, cfg
    gc.collect()
    if torch.cuda.is_available():
        torch.cuda.empty_cache()

    # return optimized metric
    return metric_value

if __name__ == "__main__":
    main()
